package com.sojson.config.filter.interfaces.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import com.sojson.config.exception.TransException;
import com.sojson.constant.Constant;
import com.sojson.util.StringUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * Xss过滤器
 * 
 * @author liu
 * @date 2020-11-02
 */
@Slf4j
public class CustomParamXssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private HttpServletRequest request;
    /** 当前访问的路径 */
    private String url;
    /** 判断白名单 */
    private static final String[] INJ_BA = {"updatedat", "updateat", "updatetime", "updateby"};
    /** 判断白名单数量 */
    private static final int INJ_BA_LEN = INJ_BA.length;
    /** 判断黑名单 */
    private static final String[] INJ_STRA = {"script", "master", "truncate", "insert", "select", "delete", "update",
        "declare", "iframe", "onreadystatechange", "alert", "atestu", "svg", "confirm", "prompt", "onload",
        "onmouseover", "onfocus", "onerror"};
    /** 判断黑名单数量 */
    private static final int INJ_STRA_LEN = INJ_STRA.length;

    /**
     * 每次请求都会从这里获取参数
     * 
     * @param request
     * @throws IOException 
     */
    public CustomParamXssHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.request = request;
        String pathInfo = request.getPathInfo();
        if (StringUtil.isBlankObject(pathInfo)) {
            pathInfo = "";
        }
        this.url = request.getServletPath() + pathInfo;
    }

    /**
     * 获取请求头
     */
    @Override
    public String getHeader(String name) {
        return checkSQLInjectEscape(request.getHeader(name));
    }

    @Override
    public String getQueryString() {
        return checkSQLInjectEscape(request.getQueryString());
    }

    /**
     * 获取表单参数
     */
    @Override
    public String getParameter(String name) {
        return checkSQLInjectEscape(request.getParameter(name));
    }

    /**
     * 获取表单参数集合
     */
    @Override
    public String[] getParameterValues(String name) {
        String[] values = request.getParameterValues(name);
        if (values != null) {
            int length = values.length;
            String[] escapseValues = new String[length];
            for (int i = 0; i < length; i++) {
                escapseValues[i] = checkSQLInjectEscape(values[i]);
            }
            return escapseValues;
        }
        return values;
    }

    /**
     * 获取请求体参数
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 为空，直接返回
        String str = IOUtils.toString(request.getInputStream(), Constant.DEFAULT_ENCODING);
        if (StringUtil.isBlankString(str)) {
            return request.getInputStream();
        }

        // xss过滤
        str = checkSQLInjectEscape(str);
        final ByteArrayInputStream bais = new ByteArrayInputStream(str.getBytes(Constant.DEFAULT_ENCODING));
        // 返回
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }
        };
    }

    /**
     * 检查是否存在非法字符,并替换html标签
     *
     * @param str 被检查的字符串
     */
    private String checkSQLInjectEscape(String str) {
        checkSQLInject(str);
        return escape(str);
    }

    /**
     * 检查是否存在非法字符,防止SQL注入
     *
     * @param str 被检查的字符串
     * @return ture-字符串中存在非法字符,false-不存在非法字符
     */
    private boolean checkSQLInject(String str) {
        // 如果传入空串则认为不存在非法字符
        if (StringUtil.isBlankString(str)) {
            return false;
        }

        // sql不区分大小写
        str = str.toLowerCase();

        // 是否存在非法字符
        boolean isExists = false;
        int i;
        // 如果存在非法字符就标记并跳出循环
        for (i = 0; i < INJ_STRA_LEN; i++) {
            if (str.indexOf(INJ_STRA[i]) >= 0) {
                isExists = true;
                break;
            }
        }

        // 如果存在非法字符就先去除所有白名单再重新判断
        if (isExists) {
            // 将所有白名单替换为空
            for (int j = 0; j < INJ_BA_LEN; j++) {
                String ba = INJ_BA[j];
                if (str.indexOf(ba) >= 0) {
                    str = str.replace(ba, "");
                }
            }

            // 如果还有非法字符就返回错误信息
            for (int j = i; j < INJ_STRA_LEN; j++) {
                String stra = INJ_STRA[j];
                if (str.indexOf(stra) >= 0) {
                    log.info("xss防攻击拦截url: " + this.url + ",原因:特殊字符,传入str=" + str + ",包含特殊字符: " + stra);
                    throw new TransException("参数不能包含非法字符: " + stra);
                }
            }
        }

        return false;
    }

    /**
     * 将容易引起xss漏洞的半角字符直接替换成全角字符
     *
     * @param str
     * @return
     */
    private String escape(String str) {
        if (StringUtils.isEmpty(str)) {
            return str;
        }
        StringBuilder sb = new StringBuilder(str.length() + 16);
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            switch (c) {
                case '>':
                    sb.append("&gt;");
                    break;
                case '<':
                    sb.append("&lt;");
                    break;
                case '&':
                    sb.append("&amp;");
                    break;
                // case '\'':
                // sb.append("'");
                // break;
                // case '\"':
                // sb.append("&quot;");
                // break;
                default:
                    sb.append(c);
                    break;
            }
        }
        return sb.toString();
    }

}